package com.example.service;

import com.example.common.Gender;
import com.example.entity.User;
import com.example.exception.BaseException;
import com.example.exception.ExceptionEnum;
import com.example.log.LogAnnotation;
import com.example.models.UserDTO;
import com.example.repository.UserRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

/**
 * 开发中避免不了会对同一个类里面的方法调用，比如有一个类Test，它的一个方法A，A再调用本类的方法B
 * （不论方法B是用public还是private修饰），
 * 但方法A没有声明注解事务，而B方法有。则外部调用方法A之后，方法B的事务是不会起作用的。这也是经常犯错误的一个地方。
 * 那为啥会出现这种情况？其实这还是由于使用Spring AOP代理造成的，
 * 因为只有当事务方法被当前类以外的代码调用时，才会由Spring生成的代理对象来管理。
 * <p>
 *
 * @link {https://juejin.cn/post/6844904096747503629}
 */
@Slf4j
@Service
public class UserService {


    @Resource
    private UserRepository userRepository;

    @Resource
    private UserService userService;

    @Resource
    private RedisTemplate<Object, Object> redisTemplate;

    /**
     * 模拟死锁 100个线程来执行这个事务
     */
    @Transactional(rollbackFor = Exception.class)
    public void updateData0() {
        User user1 = userRepository.findUserById("563");
        log.info("user:{}", user1);
        user1.setScore(new Random().nextInt(1000));
        user1.setUserName(Thread.currentThread().getName());
        log.info("update user1:{}", user1);
        userRepository.update(user1);
        log.info("updateData0");
    }


    /**
     * 模拟死锁
     */
    @Transactional(rollbackFor = Exception.class)
    public void updateData1() throws InterruptedException {
        User user1 = userRepository.findUserById("108");
        log.info("original user1:{}", user1);
        User user2 = userRepository.findUserById("158");
        log.info("original user2:{}", user2);
        user1.setScore(new Random().nextInt(1000));
        user2.setScore(new Random().nextInt(1000));
        user1.setUserName(Thread.currentThread().getName());
        user2.setUserName(Thread.currentThread().getName());
        log.info("update user1:{}", user1);
        log.info("update user2:{}", user2);
        userRepository.update(user1);
        TimeUnit.SECONDS.sleep(60);
        log.info("updateData1");
        userRepository.update(user2);
        log.info("updateData1--------->1");
    }

    /**
     * 模拟死锁
     */
    @Transactional(rollbackFor = Exception.class)
    public void updateData2() throws InterruptedException {
        User user1 = userRepository.findUserById("108");
        log.info("user1:{}", user1);
        User user2 = userRepository.findUserById("158");
        log.info("user2:{}", user2);
        user1.setScore(new Random().nextInt(1000));
        user2.setScore(new Random().nextInt(1000));
        user1.setUserName(Thread.currentThread().getName());
        user2.setUserName(Thread.currentThread().getName());
        log.info("update user1:{}", user1);
        log.info("update user2:{}", user2);
        userRepository.update(user2);
        TimeUnit.SECONDS.sleep(60);
        log.info("updateData2");
        userRepository.update(user1);
        log.info("updateData2----->2");
    }

    /**
     * updateData3和updateData4避免了死锁 但是获取不到锁
     * CannotAcquireLockException
     */
    @Transactional(rollbackFor = Exception.class)
    public void updateData3() throws InterruptedException {
        User user1 = userRepository.findById("259");
        log.info("user1:{}", user1);
        User user2 = userRepository.findById("454");
        log.info("user2:{}", user2);
        user1.setScore(new Random().nextInt(1000));
        user2.setScore(new Random().nextInt(1000));
        user1.setUserName(Thread.currentThread().getName());
        user2.setUserName(Thread.currentThread().getName());
        log.info("update user1:{}", user1);
        log.info("update user2:{}", user2);
        userRepository.update(user1);
        TimeUnit.SECONDS.sleep(60);
        log.info("updateData3");
        userRepository.update(user2);
        log.info("updateData3----->3");
    }

    @Transactional(rollbackFor = Exception.class)
    public void updateData4() throws InterruptedException {
        User user1 = userRepository.findById("259");
        log.info("user1:{}", user1);
        User user2 = userRepository.findById("454");
        log.info("user2:{}", user2);
        user1.setScore(new Random().nextInt(1000));
        user2.setScore(new Random().nextInt(1000));
        user1.setUserName(Thread.currentThread().getName());
        user2.setUserName(Thread.currentThread().getName());
        log.info("update user1:{}", user1);
        log.info("update user2:{}", user2);
        userRepository.update(user2);
        TimeUnit.SECONDS.sleep(60);
        log.info("updateData4");
        userRepository.update(user1);
        log.info("updateData4--------->4");
    }


    @Transactional(rollbackFor = Exception.class)
    public void updateData5() throws InterruptedException {
        User user1 = userRepository.findById("908");
        log.info("user1:{}", user1);
        User user2 = userRepository.findById("923");
        log.info("user2:{}", user2);
        user1.setScore(new Random().nextInt(1000));
        user2.setScore(new Random().nextInt(1000));
        user1.setUserName(Thread.currentThread().getName());
        user2.setUserName(Thread.currentThread().getName());
        log.info("update user1:{}", user1);
        log.info("update user2:{}", user2);
        userRepository.update(user2);
        TimeUnit.SECONDS.sleep(10);
        log.info("updateData5");
        userRepository.update(user1);
        log.info("updateData5--------->5");
    }

    @Transactional(rollbackFor = Exception.class)
    public void updateData6() throws InterruptedException {
        User user1 = userRepository.findById("908");
        log.info("user1:{}", user1);
        User user2 = userRepository.findById("923");
        log.info("user2:{}", user2);
        user1.setScore(new Random().nextInt(1000));
        user2.setScore(new Random().nextInt(1000));
        user1.setUserName(Thread.currentThread().getName());
        user2.setUserName(Thread.currentThread().getName());
        log.info("update user1:{}", user1);
        log.info("update user2:{}", user2);
        userRepository.update(user2);
        TimeUnit.SECONDS.sleep(15);
        log.info("updateData6");
        userRepository.update(user1);
        log.info("updateData6--------->6");
    }


    /**
     * 演示Cache-Aside的问题
     * 假设有A、B两个请求，请求A做更新操作，请求B做查询读取操作。
     */
    public void cacheAsideProblem() {
        new Thread(() -> {
            updateUser("465");
        }, "AAAAAAAAAAA").start();

        new Thread(() -> {
            User user = getUser("465");
            log.info("user:{}", user);
        }, "BBBBBBBBBBBB").start();

    }

    @Cacheable(cacheNames = "cache-aside", key = "'user:'+#uid")
    public User getUser(String uid) {
        log.info("uid:{}", uid);
        User user = userRepository.findUserById(uid);
        if (user == null) {
            throw new BaseException(ExceptionEnum.USER_NOT_FOUND);
        }
        return user;
    }

    @CacheEvict(cacheNames = "cache-aside", key = "'user:'+#uid")
    public void updateUser(String uid) {
        log.info("uid:{}", uid);
        User user1 = userRepository.findById(uid);
        log.info("user1:{}", user1);
        user1.setScore(new Random().nextInt(1000));
        user1.setUserName(Thread.currentThread().getName());
        log.info("update user1:{}", user1);
        userRepository.update(user1);
        // 延迟双删【操作完了-->再删除一次】。

        //redisTemplate.delete("cache-aside::user:" + uid);
        // 升级一下  第二次异步删除
        try {
            CompletableFuture.supplyAsync(() -> {
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return redisTemplate.delete("cache-aside::user:" + uid);
            }).whenComplete((t, u) -> {
                log.info("t:{},u:{}", t, u);
            }).exceptionally(e -> {
                log.info("e:{}", e.getMessage());
                return null;
            }).get();
        } catch (Exception e) {
            e.printStackTrace();
        }

    }


    /**
     * 缓存失效的情况(以及解决方案)
     */
    @LogAnnotation
    @Cacheable(cacheNames = "test-cache", key = "'getUserById:'+#id")
    public UserDTO getUserById(String id) {
        UserDTO byId = this.userService.getById(id);
        return byId;
    }

    @LogAnnotation
    @Cacheable(cacheNames = "base-cache", key = "'getById:'+#id")
    @Transactional
    public UserDTO getById(String id) {
        User user = userRepository.findUserById(id);
        if (user == null) {
            throw new BaseException(ExceptionEnum.USER_NOT_FOUND);
        }
        UserDTO userDTO = new UserDTO();
        BeanUtils.copyProperties(user, userDTO);
        return userDTO;
    }


    @LogAnnotation
    public void insert1() {
        // 不回滚
        insert();

    }


    @Transactional
    public void insert2() {
        // 回滚
        insert();
    }

    // 强制调用方要有事务
    //@Transactional(propagation = Propagation.MANDATORY)
    //@PostConstruct
    public void insert() {
        log.info("insert");
        User user = new User();
        user.setUserName("test");
        int i = new Random().nextInt(1000);
        log.info("Id:{}", i);
        user.setId(i);
        //user.setGender(Gender.MAN);
        userRepository.insert(user);
        //int n = 1 / 0;
    }

}
